How the Commonwealth of Massachusetts Covering A Greater Proportion of Public School Funding Through State Level Revenue Would Help Reduce NIMBYism

Author: Conor Sheehy

Some Implications for Economic Development & Relevant Massachusetts Education Data, Charts, and Tables

The United States and other anglophone countries like the U.K. and Australia, which share similar regulatory regimes, often face significant challenges in building major intrastate and interstate projects. This difficulty arises from the vast number of stakeholders involved and poorly thought-out laws that allow vocal minorities and special interest groups to intervene and halt progress. NIMBYism (Not In My Backyard) is a scourge on economic growth, reducing standards of living for all citizens, particularly low-income individuals. Arbitrary rules and delays prevent even commonsense and high-benefit/cost projects, such as a high-speed rail from Boston to Washington, from being realized.

While projects are challenging enough to complete when managed by federal and state officials, the outsourcing of state legislative power over land use and zoning to municipal authorities presents nearly insurmountable economic development, city growth, and housing cost challenges. Individual property owners have powerful incentives to ensure housing scarcity to prop up housing prices and protect local resources like school funding, driving NIMBYism. This, in turn, leads to project delays and increased costs. When beneficial projects do get off the ground, they often come in far above the initial budget due to lawsuits and lack of cooperation, and they take years or even decades longer to complete compared to countries such as France and Japan.

At all levels of government in the United States, we arbitrarily subsidize homeownership rather than prioritizing housing affordability. As an indirect consequence of producing laws intending to make homeownership more accessible, we've actually failed at that goal while also inadvertently making housing more expensive and scarce for both homebuyers and renters. Homeowners reasonably focus intensely on the downside risks to their home equity, which further fuels NIMBYism. The economics of restricting development are simple: by using legal maneuvering and restrictions to block new residential development, output of residential units collapses, and in strong labor markets with strong university systems like Boston and New York City, reliably increasing demand for shelter drives up prices for the existing housing stock. For existing homeowners and landlords, their assets appreciate. For renters, rents reliably increase year after year, and on the margin, the lowest income renters are forced to make hard choices: move to a city with a worse labor market and cheaper housing, get more roommates, or become the working homeless.

The politics of zoning and permitting is fraught. Many NIMBYs are not unreasonable people. Rather than assuming NIMBYs are naturally opposed to development, it is clear that government policy has distorted individual incentives, making people more likely to block development than they would otherwise.

I aim to tackle just one policy that makes development so difficult in Massachusetts: Local Property Taxes being used to fund public schools. I propose two pathways to address this issue: pushing as much collection of revenue/funding as possible to the state level, and replacing property tax with land value tax as a less distortionary replacement. The first pathway can be implemented through various state-level tax approaches, while the second pathway is more speculative and would require a larger adjustment. My approach can be extended to any other state that provides per pupil spending data by district/town/city.

Currently, most municipal revenue in Massachusetts is collected through property tax, Chapter 70 state foundation budget school aid, and user fees for municipal services like sewer and water. Relative to land value tax, property tax is suboptimal because it decreases marginal investment in property improvements. If a dilapidated building is improved or a parking lot is replaced with a building, the owner faces a higher tax bill in perpetuity. The government ought to encourage these sorts of improvements rather than taxing them. While there are concerns around implementation, a land value tax would be preferable to property tax because land supply is typically highly inelastic/fixed. Land value tax rewards the efficient use of land and prompts owners that are using valuable land inefficiently to develop or sell to someone who will. This would shift individual property owners' incentives toward up-zoning, remove disincentives for making home renovations, and potentially soften NIMBY opposition.

Property tax assessments already require land value assessments as a component, however, the primary objective of current assessments is the accurate valuation of the overall property, rather than the residual value of the land. Barring environmental pollution or large geophysical disparities, the assessed value per acre of directly adjacent equivalent plots ought to be priced similarly. An evaluation of feasibility needs to be done, but given that property tax assessments are already done on a large scale despite being done on illiquid assets, determining residual values of land should not be a dealbreaker if the political will is there. This document does not assess the distributional effects of the land value tax - to do that I need to first collect and aggregate data on land values to establish some reasonable baseline level of accuracy. An attractive political feature of a land value tax is that the legislature is reclaiming some control over land use and zoning from municipalities indirectly and could split the Mass Municipal Association's alignment based on their change in relative tax burdens under the new approach.

The main problem I foresee with this approach is that K-12 education funding varies considerably by town and is primarily funded through property tax and Massachusetts Foundation budget assistance from the state government, so it's not possible to directly phase out property tax at the state level. However, based on my analytics below, I believe that substantial progress can be made to decouple resistance to up-zoning and deregulation based on local pooling of school funding, while preserving the current variation in per-pupil spending.

Using the data aggregations below, my idea is this: phase in a land value tax (with reasonable deadlines for municipal adjustments) that replaces roughly 13,500 to 15,000 USD per student (1st to 5th percentile per pupil spending) and allocate to schools based on headcount statewide. All towns would retain the ability to levy a local property tax or replace their property tax with an additional local land value tax. Municipalities are most likely to respond to the subsidy by reducing local property tax levels to keep per-pupil spending constant. Looking at the median town which spends ~$18,700/pupil, roughly 80% (15k/18.7k) would be state-funded. Towns like Newton that spend closer to 25k would see ~60% replaced. A large proportion of schooling costs related to inflows of children from new local developments would be collectively handled by the entire state, and thus lessen hostility to heterogenous population growth rates by town. Additionally, while an outlier individual town could see a 20-30% year-over-year increase in population with new developments in a much looser regulatory regime, this is highly unlikely at the state level due to its sheer size.

In the short run, low density relative to land value places like Brookline could potentially see increased taxes overall. It is notable however, that many Homeowners and Renters may see decreases in their relative tax burden in municipalities with large underutilized spaces like empty parking lots, golf courses in urban areas, or large differences in lot size by area of town (Brookline for instance has a dense area near the Greenline C branch, while the southern part of the town has more mini-mansions and larger lot sizes).

I've attached charts and tables with 2021 Massachusetts education spending data (some are charter, some are districts, and some are towns/cities so it isn't totally consistent). The table entitled "QuantileData" shows quantile information for Total Expenditures per Pupil per school across the data and high and low estimates for state revenue collections needed to replace the per-pupil expenditures for all students statewide for various levels of per pupil spending (High of 992k students and low of 883k students. 65 to 100k variation based on which source/year is used and which private/parochial school figures are used).

The State already provides about $6.6 Billion of state assistance through Chapter 70 foundation budget requirements, so this policy could be coupled with cuts to other state taxes that are more distortionary, like income. Since Massachusetts is more generous than most other states in education spending for children, as more housing gets built and prices come down, out-of-state resident parents would be incentivized to move into Massachusetts. While this could at a later date present a fiscal burden, this is dependent on the characteristics of the movers; this effect wouldn't play out in a significant way until housing costs have already dipped considerably since Mass already has some of the highest housing costs in the country and it will take quite some time for major policy changes to play out.

For additional context, the Mass State budget is anticipated at ~ 56-58 billion for 2024. Mass GDP is projected at 720 billion annualized for 2023, so bear in mind that these are 2021 Education/2023 GDP figures, so I'm probably off by about 0.1-0.3% for % GDP figures in the QuantileData table. Beyond the pair plot (selected scatterplots) at the bottom of the page are several charts related to school characteristics and their relationship to the ethnic makeup of students, as well as data aggregations across the state relative to the town of Newton, MA, where the Newton Teacher's Association recently held a strike.

In [ ]:
'''Data is from the Massachusetts Department of Education here:
https://profiles.doe.mass.edu/statereport/ '''

# Import Relevant Packages
import platform
import numpy as np
import matplotlib as plt
import pandas as pd
import seaborn as sns
import seaborn.objects as so
import re
import warnings

# Package Versions For Current Run
print('python ' + platform.python_version())
print('numpy: ' + np.__version__)
print('matplotlib: ' + plt.__version__)
print('pandas: ' + pd.__version__)
print('seaborn: ' + sns.__version__)
print('regex: ' + re.__version__)
python 3.11.5
numpy: 1.24.3
matplotlib: 3.8.0
pandas: 2.1.1
seaborn: 0.12.2
regex: 2.2.1
In [ ]:
'''
import warnings in case they pop up on a future run'''
  # Ignore DTypeWarnings and FutureWarnings 
warnings.filterwarnings("ignore", "is_categorical_dtype")
warnings.filterwarnings("ignore", "use_inf_as_na")
warnings.simplefilter("ignore", category=FutureWarning) 


# Import Spending Figures
MassPerPupilSpend = pd.read_excel('PerPupilExpenditures.xlsx', sheet_name='Per Pupil Expenditures', skiprows=1, nrows=399)
MassPerPupilSpend = MassPerPupilSpend.astype('string')

# Separate to Speed up String Replace Operations and data type changes
District_Name_Code = MassPerPupilSpend[['District Name','District Code','In-District Expenditures']]
MassPerPupilSpend = MassPerPupilSpend.drop(['District Name','District Code'],axis=1)

District_Name_Code['In-District Expenditures'].replace(['\$',' ',','],'',regex=True,inplace=True)
District_Name_Code['District Code'] = District_Name_Code['District Code'].astype('int')
District_Name_Code['In-District Expenditures'] = District_Name_Code['In-District Expenditures'].astype('float')

MassPerPupilSpend = MassPerPupilSpend.replace(['\$',' ',','],'',regex=True)
MassPerPupilSpend = MassPerPupilSpend.astype('float')

# Merge Constituent Dataframes
MassPerPupilSpend = District_Name_Code.merge(MassPerPupilSpend,on='In-District Expenditures')

# Sort Dataframe by Pupils and Expenditures Per Pupil
MassPerPupilSpend = MassPerPupilSpend.sort_values(['Total Pupil FTEs','Total Expenditures per Pupil'], ascending=False)

MassPerPupilSpend.set_index('District Name')
MassPerPupilSpend.info()
<class 'pandas.core.frame.DataFrame'>
Index: 399 entries, 398 to 311
Data columns (total 8 columns):
 #   Column                              Non-Null Count  Dtype  
---  ------                              --------------  -----  
 0   District Name                       399 non-null    string 
 1   District Code                       399 non-null    int64  
 2   In-District Expenditures            399 non-null    float64
 3   Total In-district FTEs              399 non-null    float64
 4   In-District Expenditures per Pupil  399 non-null    float64
 5   Total Expenditures                  399 non-null    float64
 6   Total Pupil FTEs                    399 non-null    float64
 7   Total Expenditures per Pupil        399 non-null    float64
dtypes: float64(6), int64(1), string(1)
memory usage: 28.1 KB
In [ ]:
# Import Teacher Salary Dataset
TeacherData = pd.read_excel('TeacherSalaries.xlsx', sheet_name='Teacher Salaries', skiprows=1, nrows=399)

# Convert to Strings for Regex 
TeacherData['District Name'] = TeacherData['District Name'].astype('string')
TeacherData['Salary Totals'] = TeacherData['Salary Totals'].astype('string')
TeacherData['Average Salary'] = TeacherData['Average Salary'].astype('string')
TeacherData['FTE Count'] = TeacherData['FTE Count'].astype('string')

# Regex String Replacement/Cleanup, replace does full matching without regex
TeacherData['Salary Totals'].replace(['\$',' ',','],'',regex=True,inplace=True)
TeacherData['Average Salary'].replace(['\$',' ',','],'',regex=True,inplace=True)
TeacherData['FTE Count'].replace(['\$',' ',','],'',regex=True,inplace=True)

# Conversion back to Floats
TeacherData['Salary Totals'] = TeacherData['Salary Totals'].astype('float')
TeacherData['Average Salary'] = TeacherData['Average Salary'].astype('float')
TeacherData['FTE Count'] = TeacherData['FTE Count'].astype('float')

TeacherData
Out[ ]:
District Name District Code Salary Totals Average Salary FTE Count
0 Abby Kelley Foster Charter Public (District) 4450000 7586419.00 66422.00 114.20
1 Abington 10000 12181431.00 92005.00 132.40
2 Academy Of the Pacific Rim Charter Public (Dis... 4120000 3503450.00 67505.00 51.90
3 Acton-Boxborough 6000000 36991103.00 91268.00 405.30
4 Acushnet 30000 5221793.00 76566.00 68.20
... ... ... ... ... ...
391 Woburn 3470000 32234181.00 89168.00 361.50
392 Worcester 3480000 171799156.00 93339.00 1840.60
393 Worthington 3490000 322221.00 43543.00 7.40
394 Wrentham 3500000 6481188.00 87348.00 74.20
395 State Totals 0 6461729083.00 86118.00 75033.20

396 rows × 5 columns

In [ ]:
# Drop State Totals for Data Visualizations by City/Town
TownsOnlySpend = MassPerPupilSpend[MassPerPupilSpend['District Name'] != 'State Totals']
TownsOnlySpend

# Remove State Totals for Data Visualizations
TownsOnlyTeachers = TeacherData[TeacherData['District Name'] != 'State Totals']

# Generate Primary Dataset with Teacher + Student Data
AllTowns = TownsOnlySpend.merge(TownsOnlyTeachers,on=['District Name','District Code'])

# Proportion of District Expenditures going to Teachers
AllTowns['TeacherProportion'] = AllTowns['Salary Totals']/AllTowns['Total Expenditures']

# Student Teacher Ratio Calculation
AllTowns['StudentTeacherRatio'] = AllTowns['Total In-district FTEs']/AllTowns['FTE Count']

AllTowns.info()
AllTowns
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 395 entries, 0 to 394
Data columns (total 13 columns):
 #   Column                              Non-Null Count  Dtype  
---  ------                              --------------  -----  
 0   District Name                       395 non-null    string 
 1   District Code                       395 non-null    int64  
 2   In-District Expenditures            395 non-null    float64
 3   Total In-district FTEs              395 non-null    float64
 4   In-District Expenditures per Pupil  395 non-null    float64
 5   Total Expenditures                  395 non-null    float64
 6   Total Pupil FTEs                    395 non-null    float64
 7   Total Expenditures per Pupil        395 non-null    float64
 8   Salary Totals                       395 non-null    float64
 9   Average Salary                      395 non-null    float64
 10  FTE Count                           395 non-null    float64
 11  TeacherProportion                   395 non-null    float64
 12  StudentTeacherRatio                 395 non-null    float64
dtypes: float64(11), int64(1), string(1)
memory usage: 40.2 KB
Out[ ]:
District Name District Code In-District Expenditures Total In-district FTEs In-District Expenditures per Pupil Total Expenditures Total Pupil FTEs Total Expenditures per Pupil Salary Totals Average Salary FTE Count TeacherProportion StudentTeacherRatio
0 Boston 350000 1348915645.82 48747.80 27671.31 1676837932.25 63064.70 26589.17 481668763.00 104813.00 4595.50 0.29 10.61
1 Springfield 2810000 461156621.50 24078.80 19151.98 552515849.99 29638.00 18642.14 155184202.00 74014.00 2096.70 0.28 11.48
2 Worcester 3480000 411191518.17 24019.10 17119.36 466801954.33 26958.00 17315.90 171799156.00 93339.00 1840.60 0.37 13.05
3 Lynn 1630000 248487773.52 15549.50 15980.43 290636775.52 17774.10 16351.70 94658722.00 82162.00 1152.10 0.33 13.50
4 Brockton 440000 260837443.86 15187.20 17174.82 290538465.60 17022.80 17067.61 101375149.00 102794.00 986.20 0.35 15.40
... ... ... ... ... ... ... ... ... ... ... ... ... ...
390 Hancock 1210000 989688.30 54.60 18126.16 1613159.14 98.10 16444.03 500535.00 67640.00 7.40 0.31 7.38
391 Florida 980000 1761492.07 84.60 20821.42 1935881.75 95.90 20186.46 509459.00 47172.00 10.80 0.26 7.83
392 Rowe 2530000 1817045.04 63.00 28841.98 2340252.67 84.70 27629.90 411865.00 49622.00 8.30 0.18 7.59
393 Lowell Middlesex Academy Charter (District) 4580000 2027681.89 83.90 24167.84 2027681.89 83.90 24167.84 743579.00 106165.00 7.00 0.37 11.99
394 Savoy 2630000 1144630.50 45.00 25436.23 1382283.50 64.10 21564.49 358543.00 64026.00 5.60 0.26 8.04

395 rows × 13 columns

In [ ]:
# Import Class Size and Ethnicity Dataset
ClassSizeEthnicity = pd.read_excel('ClassSizebyRaceEthnicity.xlsx', sheet_name='Class Size by Race Ethnicity Re', skiprows=1, nrows=401)

# Convert to Strings for Regex 
ClassSizeEthnicity['District Name'] = ClassSizeEthnicity['District Name'].astype('string')
ClassSizeEthnicity['Total # of Classes'] = ClassSizeEthnicity['Total # of Classes'].astype('string')
ClassSizeEthnicity['Number of Students'] = ClassSizeEthnicity['Number of Students'].astype('string')

# Regex String Replacement/Cleanup, replace does full matching without regex
ClassSizeEthnicity['Total # of Classes'].replace(['\$',' ',','],'',regex=True,inplace=True)
ClassSizeEthnicity['Number of Students'].replace(['\$',' ',','],'',regex=True,inplace=True)

# Conversion to Floats
ClassSizeEthnicity['Total # of Classes'] = ClassSizeEthnicity['Total # of Classes'].astype('float')
ClassSizeEthnicity['Number of Students'] = ClassSizeEthnicity['Number of Students'].astype('float')
In [ ]:
# Combine Ethnicity and Class Size Data with AllTowns DataFrame - Name Ethnic Analysis
EthnicAnalysis = ClassSizeEthnicity.merge(AllTowns,on=['District Name','District Code'])

# Generate Charter School Column (includes Charter in Name)
EthnicAnalysis['School Type'] = np.where(EthnicAnalysis['District Name'].str.lower().str.contains('charter'),'Charter', 'Non-Charter')
EthnicAnalysis['School Type'] = EthnicAnalysis['School Type'].astype('category')
EthnicAnalysis.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 395 entries, 0 to 394
Data columns (total 24 columns):
 #   Column                               Non-Null Count  Dtype   
---  ------                               --------------  -----   
 0   District Name                        395 non-null    string  
 1   District Code                        395 non-null    int64   
 2   Total # of Classes                   395 non-null    float64 
 3   Average Class Size                   395 non-null    float64 
 4   Number of Students                   395 non-null    float64 
 5   African American %                   395 non-null    float64 
 6   Asian %                              395 non-null    float64 
 7   Hispanic %                           395 non-null    float64 
 8   White %                              395 non-null    float64 
 9   Native American %                    395 non-null    float64 
 10  Native Hawaiian, Pacific Islander %  395 non-null    float64 
 11  Multi-Race, Non-Hispanic %           395 non-null    float64 
 12  In-District Expenditures             395 non-null    float64 
 13  Total In-district FTEs               395 non-null    float64 
 14  In-District Expenditures per Pupil   395 non-null    float64 
 15  Total Expenditures                   395 non-null    float64 
 16  Total Pupil FTEs                     395 non-null    float64 
 17  Total Expenditures per Pupil         395 non-null    float64 
 18  Salary Totals                        395 non-null    float64 
 19  Average Salary                       395 non-null    float64 
 20  FTE Count                            395 non-null    float64 
 21  TeacherProportion                    395 non-null    float64 
 22  StudentTeacherRatio                  395 non-null    float64 
 23  School Type                          395 non-null    category
dtypes: category(1), float64(21), int64(1), string(1)
memory usage: 71.6 KB
In [ ]:
# Format Table for 2 decimal places
pd.options.display.float_format = '{:.2f}'.format
AllTowns.describe()
Out[ ]:
District Code In-District Expenditures Total In-district FTEs In-District Expenditures per Pupil Total Expenditures Total Pupil FTEs Total Expenditures per Pupil Salary Totals Average Salary FTE Count TeacherProportion StudentTeacherRatio
count 395.00 395.00 395.00 395.00 395.00 395.00 395.00 395.00 395.00 395.00 395.00 395.00
mean 4652177.22 42630924.81 2298.60 19279.83 47803108.11 2504.15 19506.27 16358807.82 81386.70 189.96 0.34 11.78
std 6336749.34 82763645.82 3647.25 4226.71 100322337.49 4394.63 3723.96 30366458.45 13042.90 315.27 0.05 2.18
min 10000.00 989688.30 45.00 11417.68 1382283.50 64.10 11417.68 322221.00 43543.00 5.60 0.15 6.60
25% 1595000.00 11431131.18 569.80 16395.80 11752696.78 597.60 16884.49 3840204.00 73208.00 48.80 0.31 10.53
50% 3040000.00 25705388.00 1372.20 18279.46 27487113.02 1452.20 18731.99 9246816.00 82721.00 113.20 0.35 11.76
75% 6015000.00 48611728.96 2702.05 21162.43 53580324.12 2830.80 21266.46 19611623.50 89558.50 229.05 0.37 12.86
max 35180000.00 1348915645.82 48747.80 43111.71 1676837932.25 63064.70 35113.70 481668763.00 117960.00 4595.50 0.50 26.22

Most Important

In [ ]:
# Generate Quantiles for Evaluation
QuantileData = TownsOnlySpend[['Total Expenditures per Pupil','Total Pupil FTEs']].quantile([0.01,0.05,0.25,0.5,0.75,0.95,0.99])
QuantileData['High Estimate StateWide Revenue (Billions)'] = QuantileData['Total Expenditures per Pupil']*992854/1000000000
QuantileData['High Mass GDP%'] = QuantileData['High Estimate StateWide Revenue (Billions)']/720*100
QuantileData['Low Estimate StateWide Revenue (Billions)'] = QuantileData['Total Expenditures per Pupil']*883000/1000000000
QuantileData['Low Mass GDP%'] = QuantileData['Low Estimate StateWide Revenue (Billions)']/720*100
QuantileData
Out[ ]:
Total Expenditures per Pupil Total Pupil FTEs High Estimate StateWide Revenue (Billions) High Mass GDP% Low Estimate StateWide Revenue (Billions) Low Mass GDP%
0.01 13654.55 98.03 13.56 1.88 12.06 1.67
0.05 15136.86 167.55 15.03 2.09 13.37 1.86
0.25 16856.38 597.40 16.74 2.32 14.88 2.07
0.50 18726.81 1442.80 18.59 2.58 16.54 2.30
0.75 21276.39 2815.00 21.12 2.93 18.79 2.61
0.95 26312.17 7022.00 26.12 3.63 23.23 3.23
0.99 32868.30 17045.34 32.63 4.53 29.02 4.03
In [ ]:
# Set Chart Styles
sns.set_style('darkgrid')
sns.color_palette('colorblind')

# ScatterPlot
ax = sns.scatterplot(TownsOnlySpend, x='Total Pupil FTEs',y='Total Expenditures per Pupil')
ax.set_title('$15,000 of State Money per Student Preserves Local Variation in School Funding')
ax.axhspan(0,QuantileData.iloc[0,0], alpha=0.2)
ax.text(40000,QuantileData.iloc[0,0],'1st Percentile Spend/Pupil 13654', ha='center', va='bottom')
ax.axhspan(0,QuantileData.iloc[1,0], alpha=0.2)
ax.text(45000,QuantileData.iloc[1,0],'5th Percentile Spend/Pupil 15136', ha='center', va='bottom')
ax.axhline(QuantileData.iloc[2,0])
ax.text(50000,QuantileData.iloc[2,0],'Median Total Spend/Pupil 18726', ha='center', va='bottom')
ax.text(0,0,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Out[ ]:
Text(0, 0, 'Conor Sheehy')
No description has been provided for this image
In [ ]:
# 10k pupil zoom in
tenthousandpupilmask = TownsOnlySpend[TownsOnlySpend['Total Pupil FTEs']<10000]

# Zoomed-In Version From Above + Formatting
ax = sns.scatterplot(tenthousandpupilmask, x='Total Pupil FTEs',y='Total Expenditures per Pupil')
ax.set_title('$15,000 of State Money per Student Preserves Local Variation in School Funding (Zoomed in on < 10,000 Pupils)')
ax.axhspan(0,QuantileData.iloc[0,0], alpha=0.2)
ax.text(10500,QuantileData.iloc[0,0],'1st Percentile Spend/Pupil 13,654', ha='left', va='top')
ax.axhspan(0,QuantileData.iloc[1,0], alpha=0.2)
ax.text(10500,QuantileData.iloc[1,0],'5th Percentile Spend/Pupil 15,136', ha='left', va='top')
ax.axhline(QuantileData.iloc[2,0])
ax.text(10500,QuantileData.iloc[2,0],'Median Total Spend/Pupil 18,726', ha='left', va='top')
ax.text(0,0,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Out[ ]:
Text(0, 0, 'Conor Sheehy')
No description has been provided for this image
In [ ]:
# Order 2 Regression plot - Potentially Spurious Relationship, especially given low sample of large schools
regorder2 = sns.regplot(TownsOnlySpend, x='Total Pupil FTEs',y='Total Expenditures per Pupil',order=2,ci=None)
regorder2.text(50000,10000,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Out[ ]:
Text(50000, 10000, 'Conor Sheehy')
No description has been provided for this image

Additional Information On Teacher Salaries and School Quality Proxies

In [ ]:
# Expenditures and Teacher Resourcing per Student
stuteachperpupil = sns.scatterplot(EthnicAnalysis, x='Total Expenditures per Pupil', y='StudentTeacherRatio', hue = 'School Type', hue_order=['Non-Charter','Charter'])
stuteachperpupil.text(11000,7.5,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Out[ ]:
Text(11000, 7.5, 'Conor Sheehy')
No description has been provided for this image
In [ ]:
(so.Plot(EthnicAnalysis, x='Total Expenditures per Pupil', y='StudentTeacherRatio', text='District Name')
        .add(so.Dot())
        .add(so.Text(valign='top'))
        .layout(size=(30,30)))
Out[ ]:
No description has been provided for this image
In [ ]:
sns.lmplot(EthnicAnalysis, x='Total Expenditures per Pupil', y='TeacherProportion', hue = 'School Type',hue_order=['Non-Charter','Charter'], ci=False)
Out[ ]:
<seaborn.axisgrid.FacetGrid at 0x15b809790>
No description has been provided for this image
In [ ]:
#Significant Variation in Average Teacher Salaries and Total Expenditures Per Pupil
teachersalaryperpupil = sns.scatterplot(EthnicAnalysis,x='Total Expenditures per Pupil',y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'])
teachersalaryperpupil.text(30000,42000,'Conor Sheehy',alpha=.2)
Out[ ]:
Text(30000, 42000, 'Conor Sheehy')
No description has been provided for this image
In [ ]:
sns.lmplot(EthnicAnalysis, x= 'Total Expenditures per Pupil', y= 'Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'],  ci=False)
Out[ ]:
<seaborn.axisgrid.FacetGrid at 0x15b914a90>
No description has been provided for this image
In [ ]:
sns.lmplot(EthnicAnalysis, x= 'Total Expenditures per Pupil', y= 'Average Salary', col='School Type', hue ='School Type', hue_order=['Non-Charter','Charter'], ci=None)
Out[ ]:
<seaborn.axisgrid.FacetGrid at 0x15b8c5c10>
No description has been provided for this image
In [ ]:
#Plot Town Names, Expenditures per Pupil and Average Teacher Salary
(so.Plot(EthnicAnalysis, x="Total Expenditures per Pupil", y="Average Salary", text="District Name")
        .add(so.Dot())
        .add(so.Text(valign="top"))
        .layout(size=(30,30)))
Out[ ]:
No description has been provided for this image
In [ ]:
#Plot Town Names, Total Pupil FTEs, and Expenditure per Pupil
(so.Plot(AllTowns, x="Total Pupil FTEs", y="Total Expenditures per Pupil", text="District Name")
        .add(so.Dot())
        .add(so.Text(halign="left"))
        .layout(size=(30,10)))
Out[ ]:
No description has been provided for this image
In [ ]:
#Please Do Not Draw Causal Relationships from the Scatterplots
#Ignore Relationship between Average Salary and TeacherProportion (of total expenditure) since they are non-independent
ForPairplot = EthnicAnalysis[['Total Pupil FTEs','Total Expenditures per Pupil','Average Salary','FTE Count','School Type','TeacherProportion','StudentTeacherRatio']]
sns.pairplot(ForPairplot, hue = 'School Type', hue_order=['Non-Charter','Charter'], diag_kind='hist')
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x15c104250>
No description has been provided for this image
In [ ]:
EthnicPairplot = EthnicAnalysis[['Average Class Size','African American %','Asian %','Hispanic %','White %','Native American %','Native Hawaiian, Pacific Islander %','Multi-Race, Non-Hispanic %','School Type']]
sns.pairplot(EthnicPairplot, hue = 'School Type',hue_order=['Non-Charter','Charter'], diag_kind='hist')
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x15ba2f690>
No description has been provided for this image

Referring to the Above: Are children in Charter Schools more likely to be better resourced than in public schools? The higher the black student percentage of the district the more likely it is to have less teachers per student.

In [ ]:
sns.boxplot(data=EthnicAnalysis, x='School Type', y='African American %',hue_order=['Non-Charter','Charter'])
EthnicAnalysis.groupby('School Type')['African American %'].describe()
Out[ ]:
count mean std min 25% 50% 75% max
School Type
Charter 78.00 27.24 25.22 0.40 5.33 17.80 48.45 87.70
Non-Charter 317.00 4.15 6.28 0.00 1.20 2.30 4.60 60.90
No description has been provided for this image

Mass Schools with more than 10% black students are outliers among public schools.

In [ ]:
sns.scatterplot(data=EthnicAnalysis, x='White %', y='Average Class Size', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
Out[ ]:
<Axes: xlabel='White %', ylabel='Average Class Size'>
No description has been provided for this image
In [ ]:
ax1 = sns.scatterplot(data=EthnicAnalysis, x='White %', y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
sns.move_legend(ax1, 'upper left', bbox_to_anchor=(1, 1))
No description has been provided for this image
In [ ]:
ax2 = sns.scatterplot(data=EthnicAnalysis, x='White %', y='Total Expenditures per Pupil', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
sns.move_legend(ax2, 'upper left', bbox_to_anchor=(1, 1))
No description has been provided for this image
In [ ]:
EthnicAnalysis['NewtonTrue'] = EthnicAnalysis['District Name'].str.contains('Newton')
Newton = EthnicAnalysis[EthnicAnalysis['District Name'].str.contains('Newton')]

ExcludingNewton = EthnicAnalysis[EthnicAnalysis['NewtonTrue']==False]
ExcludingNewton
Out[ ]:
District Name District Code Total # of Classes Average Class Size Number of Students African American % Asian % Hispanic % White % Native American % ... Total Expenditures Total Pupil FTEs Total Expenditures per Pupil Salary Totals Average Salary FTE Count TeacherProportion StudentTeacherRatio School Type NewtonTrue
0 Abby Kelley Foster Charter Public (District) 4450000 713.00 18.90 1425.00 54.30 3.40 19.70 16.80 0.60 ... 24004095.80 1424.30 16853.85 7586419.00 66422.00 114.20 0.32 12.47 Charter False
1 Abington 10000 1017.00 19.80 2178.00 5.00 2.30 11.50 78.70 0.50 ... 36223802.52 2243.90 16143.23 12181431.00 92005.00 132.40 0.34 16.05 Non-Charter False
2 Academy Of the Pacific Rim Charter Public (Dis... 4120000 201.00 19.70 538.00 62.60 0.70 28.40 6.70 0.00 ... 11434614.36 539.00 21216.47 3503450.00 67505.00 51.90 0.31 10.39 Charter False
3 Acton-Boxborough 6000000 2368.00 17.10 5246.00 3.20 33.60 6.50 51.60 0.10 ... 101326511.27 5344.90 18957.61 36991103.00 91268.00 405.30 0.37 12.88 Non-Charter False
4 Acushnet 30000 400.00 15.20 913.00 1.20 1.00 4.20 89.80 0.20 ... 18413333.92 1235.10 14908.37 5221793.00 76566.00 68.20 0.28 13.53 Non-Charter False
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
390 Winthrop 3460000 661.00 20.60 1847.00 1.40 0.70 16.00 80.50 0.10 ... 30863993.00 1887.20 16354.38 11547972.00 76884.00 150.20 0.37 12.21 Non-Charter False
391 Woburn 3470000 2985.00 13.80 4332.00 7.50 7.10 12.40 68.40 0.80 ... 92125625.08 4494.40 20497.87 32234181.00 89168.00 361.50 0.35 11.90 Non-Charter False
392 Worcester 3480000 11791.00 14.10 20660.00 17.30 6.20 42.90 29.00 0.20 ... 466801954.33 26958.00 17315.90 171799156.00 93339.00 1840.60 0.37 13.05 Non-Charter False
393 Worthington 3490000 60.00 6.00 58.00 0.00 0.00 5.20 91.40 1.70 ... 1730322.56 118.70 14577.28 322221.00 43543.00 7.40 0.19 8.34 Non-Charter False
394 Wrentham 3500000 404.00 18.40 893.00 1.80 3.00 5.40 84.80 0.30 ... 18253166.12 907.80 20107.03 6481188.00 87348.00 74.20 0.36 11.56 Non-Charter False

394 rows × 25 columns

In [ ]:
MassExNewtonQuantiles = ExcludingNewton[['Average Class Size',
       'In-District Expenditures per Pupil',
       'Total Pupil FTEs', 'Total Expenditures per Pupil',
       'Average Salary', 'TeacherProportion',
       'StudentTeacherRatio','FTE Count','Salary Totals']].quantile([.25,.5,.67,.75,.833,.95,.99])


NewtonComparison = pd.concat([MassExNewtonQuantiles, Newton[['Average Class Size',
       'In-District Expenditures per Pupil',
       'Total Pupil FTEs', 'Total Expenditures per Pupil',
       'Average Salary', 'TeacherProportion',
       'StudentTeacherRatio','FTE Count','Salary Totals']]])

NewtonComparison.index = ['25%', 'Median','67%','75%','83.3%','95%','99%','Newton']
NewtonComparison.style.format(precision=2, thousands=",", decimal=".")
NewtonComparison
Out[ ]:
Average Class Size In-District Expenditures per Pupil Total Pupil FTEs Total Expenditures per Pupil Average Salary TeacherProportion StudentTeacherRatio FTE Count Salary Totals
25% 13.83 16393.53 597.40 16874.23 73133.00 0.31 10.53 48.20 3838836.00
Median 15.85 18255.37 1442.80 18726.81 82697.00 0.35 11.77 113.10 9239410.00
67% 17.10 20173.37 2292.84 20374.65 87343.17 0.36 12.52 172.71 14689377.03
75% 17.90 21092.40 2815.00 21239.89 89547.50 0.37 12.87 228.22 19532510.75
83.3% 18.74 22718.09 3902.69 22780.88 93060.63 0.38 13.50 297.22 26300303.92
95% 22.47 26630.85 6961.17 26207.71 100951.15 0.41 15.19 514.95 46204779.95
99% 31.94 33661.24 17075.39 32790.03 111053.67 0.44 17.55 1125.32 95128871.89
Newton 17.10 22499.29 12200.70 23431.12 93031.00 0.36 11.02 1091.00 101497172.00

^Comparison between Newton and All other Towns Above

More Newton Information Below:

In [ ]:
Newton = EthnicAnalysis[EthnicAnalysis['District Name'].str.contains('Newton')]
Newton
Out[ ]:
District Name District Code Total # of Classes Average Class Size Number of Students African American % Asian % Hispanic % White % Native American % ... Total Expenditures Total Pupil FTEs Total Expenditures per Pupil Salary Totals Average Salary FTE Count TeacherProportion StudentTeacherRatio School Type NewtonTrue
242 Newton 2070000 5815.00 17.10 12121.00 4.60 20.10 8.60 58.90 0.10 ... 285876115.00 12200.70 23431.12 101497172.00 93031.00 1091.00 0.36 11.02 Non-Charter True

1 rows × 25 columns

FTE refers to Full Time Equivalent, Salary is Average Teacher Salary, Teacher Proportion is the proportion of The District School budget that goes to teacher salaries. Student Teacher Ratio is a calculated ratio of Number of Students to the number of FTE Teachers. 2021 Education Data from DESE (Mass.gov)

In [ ]:
sns.histplot(EthnicAnalysis, y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'], binwidth=2500, binrange=(40000,120000))
Out[ ]:
<Axes: xlabel='Count', ylabel='Average Salary'>
No description has been provided for this image

Proportion of Total District Expenditures going to Teachers (Below)

In [ ]:
sns.histplot(EthnicAnalysis, y='TeacherProportion', hue = 'School Type', hue_order=['Non-Charter','Charter'], binwidth=.0125, binrange=(.15,.5))
Out[ ]:
<Axes: xlabel='Count', ylabel='TeacherProportion'>
No description has been provided for this image
In [ ]:
sns.scatterplot(EthnicAnalysis, x = 'StudentTeacherRatio', y= 'Average Class Size', size='Number of Students', hue = 'School Type', hue_order=['Non-Charter','Charter'])
Out[ ]:
<Axes: xlabel='StudentTeacherRatio', ylabel='Average Class Size'>
No description has been provided for this image
In [ ]:
ax = sns.scatterplot(data=EthnicAnalysis, x='Average Salary', y='Average Class Size', size='Number of Students', hue = 'School Type', hue_order=['Non-Charter','Charter'])
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
No description has been provided for this image